﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Security.Cryptography;
using System.Text;

namespace Navigation
{
	/// <summary>
	/// Protects Urls from tampering by appending a checksum to the query string. This is a string
	/// generated from the other query string parameters so if one happens to change the checksum
	/// should no longer match
	/// </summary>
	public class ChecksumNavigationShield : NavigationShield
	{
		/// <summary>
		/// Gets or sets the key that identifies the CheckSum
		/// </summary>
		[ConfigurationProperty("checksumKey", DefaultValue = "cs")]
		public string ChecksumKey
		{
			get
			{
				return (string)this["checksumKey"];
			}
			set
			{
				this["checksumKey"] = value;
			}
		}

		/// <summary>
		/// Gets or sets key used in the checksum generation routine, should be kept secret to prevent
		/// Url vulnerability
		/// </summary>
		[ConfigurationProperty("key", IsRequired = true)]
		public string Key
		{
			get
			{
				return (string)this["key"];
			}
			set
			{
				this["key"] = value;
			}
		}

		/// <summary>
		/// Gets of sets length of the generated checksum, the default value is 8
		/// </summary>
		[IntegerValidator(MinValue = 1), ConfigurationProperty("length", DefaultValue = 8)]
		public int Length
		{
			get
			{
				return (int)this["length"];
			}
			set
			{
				this["length"] = value;
			}
		}

#if NET35Plus
		/// <summary>
		/// Uses all the <paramref name="data"/> to generate a checksum and returns this data together
		/// with this checksum
		/// </summary>
		/// <param name="data">An unprotected set of key/value pairs prior to the formation
		/// of the querty string</param>
		/// <param name="historyPoint">Ignored parameter as <see cref="Navigation.ChecksumNavigationShield"/>
		/// encodes all navigation and history points</param>
		/// <returns>All query string parameters passed in <paramref name="data"/> together with
		/// generated checksum</returns>
		public override NameValueCollection Encode(NameValueCollection data, bool historyPoint)
#else
		/// <summary>
		/// Uses all the <paramref name="data"/> to generate a checksum and returns this data together
		/// with this checksum
		/// </summary>
		/// <param name="data">An unprotected set of key/value pairs prior to the formation
		/// of the querty string</param>
		/// <returns>All query string parameters passed in <paramref name="data"/> together with
		/// generated checksum</returns>
		public override NameValueCollection Encode(NameValueCollection data)
#endif
		{
			SortedDictionary<string, string> list = new SortedDictionary<string, string>();
			NameValueCollection newColl = new NameValueCollection();
			foreach (string key in data)
			{
				newColl[key] = data[key];
				list.Add(key, data[key]);
			}
			newColl.Add(ChecksumKey, GetChecksum(list));
			return newColl;
		}

#if NET35Plus
		/// <summary>
		/// Uses the <paramref name="data"/> to check the query string has not been tampered with
		/// </summary>
		/// <param name="data">Data generated by the <see cref="Encode"/> method</param>
		/// <param name="historyPoint">Ignored parameter as <see cref="Navigation.ChecksumNavigationShield"/>
		/// encodes all navigation and history points</param>
		/// <returns>All query string parameters passed in <paramref name="data"/> minus the
		/// generated checksum</returns>
		/// <exception cref="Navigation.UrlException">Any <paramref name="data"/> key is null; or the checksum
		/// does not tally</exception>
		public override NameValueCollection Decode(NameValueCollection data, bool historyPoint)
#else
		/// <summary>
		/// Uses the <paramref name="data"/> to check the query string has not been tampered with
		/// </summary>
		/// <param name="data">Data generated by the <see cref="Encode"/> method</param>
		/// <returns>All query string parameters passed in <paramref name="data"/> minus the
		/// generated checksum</returns>
		/// <exception cref="Navigation.UrlException">Any <paramref name="data"/> key is null; or the checksum
		/// does not tally</exception>
		public override NameValueCollection Decode(NameValueCollection data)
#endif
		{
			SortedDictionary<string, string> list = new SortedDictionary<string, string>();
			NameValueCollection newColl = new NameValueCollection();
			string check = null;
			foreach (string key in data)
			{
				if (key == null)
					throw new UrlException(Resources.InvalidUrl);
				if (key != ChecksumKey)
				{
					newColl[key] = data[key];
					list.Add(key, data[key]);
				}
				else
					check = data[key];
			}
			if (check == GetChecksum(list))
				return newColl;
			throw new UrlException(Resources.InvalidUrl);
		}

		private string GetChecksum(SortedDictionary<string, string> list)
		{
			StringBuilder sb = new StringBuilder();
			foreach (KeyValuePair<string, string> keyValuePair in list)
			{
				sb.Append(keyValuePair.Key);
				sb.Append(keyValuePair.Value);
			}
			sb.Append(Key);
			using (MD5 md5 = MD5.Create())
			{
				string checksum = Convert.ToBase64String(md5.ComputeHash(Encoding.ASCII.GetBytes(sb.ToString())));
				return checksum.Substring(0, Math.Min(checksum.Length, Length));
			}
		}
	}
}
